class Rudoku::RuleStats
  attr_accessor :ara, :sra
  
  def initialize
    @ara = @sra = 0
  end
end

class Rudoku::Solver
  attr_reader :rules, :field_selector, :candidate_selector
  attr_reader :guess_count, :ara_rp, :sra_rp # stats
  attr_reader :stats
  attr_accessor :dynamic_priorities
  attr_reader :priorities
  
  def initialize(rules, field_selector, candidate_selector)
    @rules = rules
    @field_selector = field_selector
    @candidate_selector = candidate_selector
    @stats = {}
    @dynamic_priorities = false
    @priorities = {}
  end
  
  def solve(sudoku, do_force = false)
    reset_stats
    reset_priorities if @dynamic_priorities
    
    if do_force
      solve_with_force(sudoku)
    else
      apply_rules(sudoku) # no bruteforcing!
    end
  end
  
  def solve_with_force(sudoku) # implements Ariadne's Thread
    return true if apply_rules(sudoku)
    return false unless sudoku.consistent? # applying the rules may have made the sudoku inconsistent. bubble up!
    
    guess_and_solve(sudoku) ? sudoku.solved? : false
  end
  
  def guess_and_solve(sudoku)     
    backup = Sudoku.new
    backup.copy_data(sudoku)
    backup_ara_rp = @ara_rp
    backup_sra_rp = @sra_rp
          
    field = @field_selector.select(sudoku).field
    return false unless field
    
    possibilities = field.possibilities

    trial_value = @candidate_selector.select(field).candidate
    field.guess(trial_value)
    @guess_count += 1
    
    # safety, might be obsolete, I dunno.
    return true if sudoku.solved?
    
    if solve_with_force(sudoku) # recursion, backtracking
      return true
    else
      # guess lead to inconsistency =>
      @ara_rp = backup_ara_rp
      @sra_rp = backup_sra_rp
      puts "BACKTRACKING, #{field.position} = #{trial_value} lead to inconsistency" if $debug
      sudoku.copy_data(backup) # restore values
      field.set_possibilities(possibilities - [trial_value])
      return solve_with_force(sudoku) # try solving again, there's more info now (one less possibility!)
    end
  end
  
  def apply_rules(sudoku)
    # iterate over all implemented rules in order of saliency
    # if one is applicable and changes the sudoku, start over
    sort_rules if @dynamic_priorities
    
    @rules.each do |rule|
      break if sudoku.solved?
      break unless sudoku.consistent?
      eliminations = rule.apply_to(sudoku)
      
      @stats[rule].ara += 1
      @ara_rp += 1
      
      eliminations.each { |e| e.perform }
      
      puts "APPLIED #{rule.name}" if sudoku.changed? && $debug
      # puts "DIDN'T APPLY #{rule.name}" if !sudoku.changed? && $debug
      
      if sudoku.changed?  # something has changed -> restart rule application
        sudoku.changed = false
        @stats[rule].sra += 1
        @sra_rp += 1
        @priorities[rule] += 1 if @dynamic_priorities
        
        retry
      else
        @priorities[rule] -= 1 if @dynamic_priorities
      end
    end
    
    return sudoku.solved?
  end
  
  def reset_stats
    @guess_count = 0
    @ara_rp = @sra_rp = 0
    @rules.each { |rule| @stats[rule] = RuleStats.new }
  end
  
  def reset_priorities
    @rules.each { |rule| @priorities[rule] = 0 }
  end
  
  def ara
    @stats.values.inject(0) { |s, v| s += v.ara }
  end
  
  def sra
    @stats.values.inject(0) { |s, v| s += v.sra }
  end
  
  def sort_rules
    @rules = @rules.sort_by { |rule| @priorities[rule] }.reverse # highest priority first
  end
end
